package com.syl.starter.itools.common.utils.io;

import com.syl.starter.itools.base.beans.FilePath;
import com.syl.starter.itools.base.constants.BaseConstant;
import com.syl.starter.itools.base.utils.CloseUtils;
import com.syl.starter.itools.base.utils.LogUtils;
import com.syl.starter.itools.common.utils.ListUtils;
import com.syl.starter.itools.common.utils.StringUtils;
import lombok.SneakyThrows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 文件操作类 应尽量使用Files 和 Path来替代file
 * nio资料 https://www.jianshu.com/p/465ecd909f8c
 * @author syl
 * @create 2018-06-28 15:35
 **/
public class FileUtils {
    public static final String JAVA_IO_TMPDIR = "java.io.tmpdir";
    private static Logger LOG = LoggerFactory.getLogger(FileUtils.class);
    private static final String USER_DIR = "user.dir";
    private static final String CLASSES = "classes";
    private static final String TEMP_FILE_DIR = "temp";
    private static final int  BUFFER_SIZE = 2 * 1024;

    /**
     * 绝对获取file 不存在则创建
     *
     * @param path 目标文件字符串路径
     * @return 路径为空返回空 否则返回该file
     */
    @SneakyThrows
    public static File getAbsoluteFile(String path){
        if(StringUtils.isEmpty(path))
            return null;
        return getAbsoluteFile(new File(path));
    }

    /**
     * 绝对获取file 不存在则创建
     * @param path 目标file的路径对象
     * @return 路径为空返回空 否则返回该file
     */
    @SneakyThrows
    public static File getAbsoluteFile(Path path){
        return getAbsoluteFile(path.toFile());
    }

    /**
     * 绝对获取file 不存在则创建
     *
     * @param file 目标file
     * @return 路径为空返回空 否则返回该file
     */
    @SneakyThrows
    public static File getAbsoluteFile(File file){
        if (file == null) {
            LOG.error("传入的file 不能为空");
            return null;
        }
        if(!file.exists()){
            if (file.isDirectory()) {
                LOG.info("是否新创建文件夹 {}", file.mkdirs());
            } else {
                LOG.info("是否新创建文件夹 {}", getParentFile(file, 1).mkdirs());
                LOG.info("是否新创建文件 {}", file.createNewFile());
            }
        }
        return file;
    }

    /**
     * 获取项目所在的目录路径
     * @return
     */
    public static String getProjectPath(){
        return System.getProperty(USER_DIR);
    }

    /**
     * 获取系统临时文件夹
     * @return
     */
    public static String getTempFileDir(){
        return System.getProperty(JAVA_IO_TMPDIR);
    }

    /**
     * 获取目标class 编译前 resources 所在的目录路径
     * 解决不同模块间获取不到真正的 resources 路径
     * @param cz this.class
     * @return
     */
    public static String getResourcePath(Class cz){
        URL url = cz.getResource(BaseConstant.EMPTY_STRING);
        String substring = url.getPath().substring(1);
        substring = substring.substring(0, substring.lastIndexOf(CLASSES));
        return substring + CLASSES + File.separator;
    }

    /**
     * 判断是否为文件，如果path为null，则返回false
     *
     * @param path 文件路径
     * @return 如果为文件true
     */
    public static boolean isFile(String path) {
        return (StringUtils.isEmpty(path)) ? false : isFile(new File(path));
    }

    /**
     * 判断是否为文件，如果file为null，则返回false
     *
     * @param file 文件路径
     * @return 如果为文件true
     */
    public static boolean isFile(File file){
        return (file == null) ? false : file.isFile();
    }

    /**
     * 判断是否为目录，如果path为null，则返回false
     *
     * @param path 目录路径
     * @return 如果为目录true
     */
    public static boolean isDirectory(String path){
        return(StringUtils.isEmpty(path)) ? false : isDirectory(new File(path));
    }

    /**
     * 判断是否为目录，如果file为null，则返回false
     *
     * @param file 文件
     * @return 如果为目录true
     */
    public static boolean isDirectory(File file) {
        return (file == null) ? false : file.isDirectory();
    }

    /**
     * 删除文件夹
     * @param folder
     * @return
     */
    public static boolean deleteFolder(File folder){
        return deleteFolder(folder.getPath());
    }

    /**
     * 删除文件夹
     * @param folderPath
     * @return
     */
    public static boolean deleteFolder(String folderPath){
        return deleteFolder(Paths.get(folderPath));
    }

    /**
     * 删除文件夹
     * @param folderPath
     * @return
     */
    public static boolean deleteFolder(Path folderPath){
        try {
            Files.walkFileTree(folderPath, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    LOG.debug("delete file: " + file.toString());
                    Files.delete(file);
                    return FileVisitResult.CONTINUE;
                }
                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    LOG.debug("delete dir: " + dir.toString());
                    Files.delete(dir);
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch(IOException e){
            LogUtils.error(LOG, "删除文件夹失败", e);
            return false;
        }
        return true;
    }

    /**
     * 压缩文件夹
     * @param folder 目标文件夹路径
     * @return 压缩文件路径
     */
    public static String compressedFolder(File folder){
        return compressedFolder(folder, folder.getName(), false);
    }

    /**
     * 压缩文件夹 并输出到指定的临时文件夹目录下
     * 且删除原文件夹
     * @param folder 目标文件夹路径
     * @param name 压缩文件名
     * @return 压缩文件路径
     */
    public static String compressedFolderTemp(File folder, String name){
        String path = name;
        if(StringUtils.isEmpty(name))
            path = System.currentTimeMillis() + BaseConstant.ZIP_SUFFIX_NAME;
        if(!StringUtils.contains(name, BaseConstant.ZIP_SUFFIX_NAME))
            path = name + BaseConstant.ZIP_SUFFIX_NAME;
        String projectPath = getProjectPath();
        String temp = projectPath + File.separator + TEMP_FILE_DIR;
        return compressedFolder(folder, temp + File.separator + path, name, true);
    }

    /**
     * 压缩文件夹
     * @param folderPath 目标文件夹路径
     * @param name 压缩文件名称
     * @return 压缩文件路径
     */
    public static String compressedFolder(String folderPath,String name){
        return compressedFolder(new File(folderPath), name, false);
    }

    /**
     * 压缩文件夹
     * @param folder 目标文件夹
     * @param name 压缩文件名称
     * @param deleteFolder 压缩完后删除原文件夹
     * @return 压缩文件路径
     */
    public static String compressedFolder(File folder,String name,boolean deleteFolder){
        if(StringUtils.isEmpty(name))
            name = System.currentTimeMillis() + BaseConstant.ZIP_SUFFIX_NAME;
        if(!StringUtils.contains(name, BaseConstant.ZIP_SUFFIX_NAME))
            name = name + BaseConstant.ZIP_SUFFIX_NAME;
        String outPath = getParentPath(folder, 1);
        outPath = outPath + File.separator + name;
        return compressedFolder(folder, outPath, name, deleteFolder);
    }

    /**
     * 压缩文件夹
     * @param folder 目标文件夹
     * @param outPath 输出路径
     * @param name 压缩文件名称
     * @param deleteFolder 压缩完后删除原文件夹
     * @return 压缩文件路径
     */
    public static String compressedFolder(File folder,String outPath,String name,boolean deleteFolder){
        if(!folder.isDirectory())
            return null;
        FileOutputStream fileOutputStream = null;
        ZipOutputStream zos = null;
        String zipName = StringUtils.substringFirstStep(name, BaseConstant.DOT);
        try {
            fileOutputStream = new FileOutputStream(outPath);
            zos = new ZipOutputStream(fileOutputStream);
            compress(folder, zos, zipName, true);
        }catch (Exception e){
            LogUtils.error(LOG, "压缩失败", e);
            return null;
        }finally {
            CloseUtils.closeAll(zos, fileOutputStream);
            if(deleteFolder) {
                LOG.debug("删除文件结果 {}", deleteFolder(folder));
            }
        }
        return outPath;
    }

    /**
     * TODO 这里应该可以优化不使用递归
     * 递归压缩方法
     * @param sourceFolder 压缩文件夹路径
     * @param zos        zip输出流
     * @param name       压缩后的名称
     * @param keepDirStructure  是否保留原来的目录结构
     * true:保留目录结构 false:所有文件跑到压缩包根目录下 文件名做随机处理
     * @throws Exception
     */
    private static void compress(File sourceFolder, ZipOutputStream zos, String name, boolean keepDirStructure) throws IOException {
        byte[] buf = new byte[BUFFER_SIZE];
        String fs = File.separator;
        if(sourceFolder.isFile()){
            // 向zip输出流中添加一个zip实体，构造器中name为zip实体的文件的名字
            FileInputStream in = null;
            try {
                zos.putNextEntry(new ZipEntry(name));
                // copy文件到zip输出流中
                int len;
                in = new FileInputStream(sourceFolder);
                while ((len = in.read(buf)) != -1){
                    zos.write(buf, 0, len);
                }
            } catch (IOException e) {
                LogUtils.error(LOG, "压缩时 copy 文件时发生异常", e);
            }finally {
                CloseUtils.closeAll(in);
                zos.closeEntry();
            }
        } else {
            File[] listFiles = sourceFolder.listFiles();
            if(listFiles == null || listFiles.length == 0){
                // 需要保留原来的文件结构时,需要对空文件夹进行处理
                if(keepDirStructure){
                    // 空文件夹的处理
                    try {
                        zos.putNextEntry(new ZipEntry(name + fs));
                    } catch (IOException e) {
                        LogUtils.error(LOG, "压缩文件时发生异常", e);
                    }
                }
            }else {
                for (File file : listFiles) {
                    // 判断是否需要保留原来的文件结构
                    if (keepDirStructure){
                        compress(file, zos, name + fs + file.getName(),keepDirStructure);
                    } else {
                        compress(file, zos, StringUtils.getUUID(true), keepDirStructure);
                    }
                }
            }
        }
    }

    /**
     * 获取路径的父路径
     * @param file 目标文件
     * @param number 上一级次数
     * @return 上number级目录路径, number超过级数时返回根目录路径
     */
    public static String getParentPath(File file, int number) {
        Path path = file.toPath();
        for (int i = 0; i < number; i++) {
            Path root = path.getRoot();
            if(root.equals(path))
                return path.toString();
            Path parent = path.getParent();
            if(parent == null)
                path.toString();
            path = parent;
        }
        return path.toString();
    }

    /**
     * 获取路径的父File
     * @param file 目标文件
     * @param number 上一级次数
     * @return 上number级目录路径, number超过级数时返回根目录File
     */
    public static File getParentFile(File file, int number) {
        return new File(getParentPath(file, number));
    }

    /**
     * 最简单文件搜索
     * @param path
     * @param fileName
     * @return
     */
    public static Path findOne(String path, String fileName){
        return findOne(path, fileName,null,null);
    }

    /**
     * 文件搜索
     * @param folderPath 目标路径
     * @param fileName 目标文件名
     * @param neglectDir 忽略文件夹列表 只要命中即跳过 可为null
     * @param neglectSuffix 忽略后缀名列表 如 xml,yml 不需要加.  可为null
     * @return
     */
    public static Path findOne(String folderPath, String fileName, String[] neglectDir, String[] neglectSuffix){
        List<FilePath> pathList = findList(folderPath, fileName, neglectDir, neglectSuffix, true);
        return ListUtils.isEmpty(pathList) ? null : pathList.get(0).getPath();
    }

    /**
     * 遍历某个文件夹
     * @param folderPath 目录路径
     * @return 返回该目录下所有文件路径
     */
    public static List<FilePath> findList(String folderPath){
        return findList(folderPath, null, null, null, false);
    }

    /**
     * 遍历某个文件夹
     * @param folderPath 目标路径
     * @param neglectDir 忽略文件夹列表 只要命中即跳过 可为null
     * @param neglectSuffix 忽略后缀名列表 如 .xml,.yml  可为null
     * @return
     */
    public static List<FilePath> findList(String folderPath, String[] neglectDir, String[] neglectSuffix){
        return findList(folderPath, null, neglectDir, neglectSuffix, false);
    }

    /**
     * 文件搜索
     * @param folderPath 目标路径
     * @param fileName 目标文件名 可为null
     * @param neglectDir 忽略文件夹列表 只要命中即跳过 可为null
     * @param neglectSuffix 忽略后缀名列表 如 .xml,.yml  可为null
     * @param lazy 是否懒惰 懒惰: 找到以后停止搜索
     * @return
     */
    public static List<FilePath> findList(String folderPath, String fileName, String[] neglectDir, String[] neglectSuffix, boolean lazy){
        Path rootPath = Paths.get(folderPath);
        List<FilePath> result = new ArrayList<>(20);
        try {
            Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    String dirName = dir.getFileName().toString();
                    String dirPath = dir.toAbsolutePath().toString();
                    if (neglectDir != null) {
                        if (Arrays.asList(neglectDir).contains(dirName)){
                            LOG.debug("*********因匹配忽略目录而跳过目录"+dirPath+"*********");
                            return FileVisitResult.SKIP_SUBTREE;
                        }
                    }
                    if (neglectSuffix != null) {
                        String[] list = new File(dirPath).list();
                        List<String> suffixList = addSuffix(list);
                        suffixList.removeAll(Arrays.asList(neglectSuffix));
                        if (suffixList.size() <= 0){
                            LOG.debug("*********因该目录下所有文件匹配忽略后缀规则 跳过目录"+dirPath+"*********");
                            return FileVisitResult.SKIP_SUBTREE;
                        }
                    }
                    if (StringUtils.isEmpty(fileName)) result.add(new FilePath(dirName, true, dir));
                    return FileVisitResult.CONTINUE;
                }

                @Override
                // 在访问任何文件之前
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    String thisFileName = file.getFileName().toString();
                    LOG.debug("*********遍历文件"+file.toAbsolutePath().toString()+"*********");
                    if (StringUtils.isEmpty(fileName)) {
                        result.add(new FilePath(thisFileName, false, file));
                    }
                    else if (StringUtils.contains(thisFileName, fileName)) {
                        result.add(new FilePath(thisFileName, false, file));
                        return lazy ? FileVisitResult.TERMINATE : FileVisitResult.CONTINUE;
                    }
                    return FileVisitResult.CONTINUE;
                }

            });
        } catch (IOException e) {
            LogUtils.error(LOG,"发生io错误",e);
        }
        return result;
    }

    private static List<String> addSuffix(String[] fileNames){
        List<String> list = new ArrayList<>(fileNames.length);
        for (String fileName : fileNames) {
            list.add(StringUtils.substringLastStep(fileName, BaseConstant.DOT));
        }
        return list;
    }

}
